<?php
/**
 * Глобальный класс управления разделами сайта
 * 
 * @category Phorm
 * @package Categories
 * @name /Phorm/Categories.php
 * @copyright phorm.ru
 * @since 30.11.2010
 * @author vladimir@kachalov.net
 */

class Phorm_Categories extends Phorm_Model {
	
	/**
	 * Объект DbTree
	 * @var Phorm_Db_Tree
	 */
	
	private $_dbtree;
	
	/**
	 * Массив полей, которые должны возвращаться при запросе списка разделов
	 * @var array
	 */
	private $_fields = array('categoryid','categoryname','menuname','categorydescr','visible','categorypath','fullpath','moduleid','clevel','cleft','cright','cparent');

	
	/**
	 * Конструктор - Инициализирует Phorm_Db_Tree
	 *
	 */
	
	public function __construct() {
		
		parent::__construct();
		
		$this->_dbtree = new Phorm_Db_Tree($this->_db,'pw_categories');
		
	}
	
	
	/**
	 * Возвращает список разделов в зависимости от текущего модуля (админка или сайт)
	 *
	 * @return array
	 */
	
	public function getList() {
		
		return $this->getModule()=='control' ? $this->getFullList() : $this->getVisibleList();
		
	}
	
	
	/**
	 * Возвращает список разделов в зависимости от текущего модуля (админка или сайт)
	 * с заменой/добавлением узлов, формированием урлов с учетом mvc-ресурсов
	 * 
	 * @param int $active Id активной категории
	 *
	 * @return array
	 */
	
	public function getRealList($active = 0) {
		
		$List = $this->getList();
		
		foreach ($List as $key=>$item) {
			
			$List[$key] = array(
				'label' => $item['menuname'],
				'title' => $item['categoryname'],
				'id' => $item['categoryid'],
				'uri' => $this->_cnf['site']['url'] . $item['fullpath'],
				'clevel' => $item['clevel'],
			);
			
			if($item['categoryid']==$active) {
				$List[$key]['active'] = true;
			}
			
			if($this->getModule()=="control") {
				$List[$key]['target'] = 'CenterFrame';
				$List[$key]['uri'] = $this->_cnf['site']['url'] . '/control/categories/view/categoryid/' . $item['categoryid'];
				if($item['visible']=='no') $List[$key]['class'] = 'ui-state-disabled';
			}
			
		}
		
		return $List;
		
	}
	
	
	/**
	 * Возвращает список разделов в виде пар id=>name в зависимости от текущего модуля (админка или сайт)
	 *
	 * @param array $options Массив условий для выборки
	 * 
	 * @return array
	 */
	
	public function getPairsList($options = array()) {
		
		$select = $this->_db->select(array('categoryid','categoryname'))
				-> from("pw_categories",$this->_fields)
				-> where("isexist='yes'")
				-> order("cleft");
				
		if($this->getModule()!='control') {
			$select -> where("fullpath_visible='yes'")
					-> where("isexist='yes'");
		}
		
		foreach ($options as $k=>$v) $select->where($k.'=?',$v);
		
		return $this->_db->fetchPairs($select);
		
	}
	
	
	/**
	 * Возвращает список всех разделов
	 *
	 * @return array
	 */
	
	public function getFullList() {
		
		$select = $this->_db->select()
				-> from('pw_categories',$this->_fields)
				-> where('isexist=?','yes')
				-> where('fullpath_exist=?','yes')
				-> order('cleft');
		
		return $this->_db->fetchAll($select);
		
	}
	
	
	/**
	 * Возвращает список видимых на сайте разделов
	 *
	 * @return array
	 */
	
	public function getVisibleList() {
		
		$select = $this->_db->select()
				-> from('pw_categories',$this->_fields)
				-> where('fullpath_visible=?','yes')
				-> where('fullpath_exist=?','yes')
				-> where('isexist=?','yes')
				-> order('cleft');
		
		return $this->_db->fetchAll($select);
		
	}
	
	
	/**
	 * Перемещает раздел в заданную вершину
	 * 
	 * @param array $FromInfo Массив данных переносимого раздела
	 * @param array $ToInfo Массив данных раздела куда переносить
	 * 
	 * @return boolean
	 */
		
	public function moveCategory($FromInfo,$ToInfo) {
		
		$this->_db->beginTransaction();
		
		try {
			
			$this->_dbtree->MoveAll($FromInfo['categoryid'],$ToInfo['categoryid']);
			$this->updateFullPath($FromInfo['categoryid']);
			
			/**
			 * Логирование
			 */
			$this->setLog('move',
				array('url' => '/control/categories/edit/categoryid/' . (int)$FromInfo['categoryid'] . '/',
					  'name' => $FromInfo['categoryname'],
					  'tablename' => 'pw_categories',
					  'identity' => (int)$FromInfo['categoryid'],
					  'fieldname' => 'categoryid'));
			
			/**
			 * Очистка кеша 
			 */
			$this->getCache('categories')->clean();
			
			$this->_db->commit();
			
			return true;
			
		} catch (Exception $e){
			
			$this->_db->rollBack();
			$this->_errors[] = $e->getMessage();
			
		}
			
		return false;
			
	}
	
	
	/**
	 * Вовращает информацию о разделе на основе пути
	 *
	 * @param string $path Путь раздела (должен начинаться с / и заканчиваться /)
	 * 
	 * @return array
	 */
	
	public function getCategoryInfoByPath($path) {
		
		$select = $this->_db->select()
				-> from('pw_categories')
				-> where('isexist=?','yes')
				-> where('fullpath=?',$path);
				
		if($this->getModule()!='control') $select -> where('fullpath_visible=?','yes');
		
		return $this->_db->fetchRow($select);
		
	}
	
	
	/**
	 * Возвращает список связанных разделов
	 * 
	 * @param int $categoryid Id раздела
	 * @param array $fromcategories Список разделов участвующих в выборке
	 * 
	 * @return array
	 */
		
	function getDependedCategoriesList($categoryid,$fromcategories=array()) {
		
		$select = $this->_db->select()
				-> from("pw_categories_depends",array("IF(categoryid1=".(int)$categoryid.",categoryid2,categoryid1)"))
				-> where("categoryid1!=categoryid2")
				-> where((int)$categoryid." IN(categoryid1,categoryid2)");
		
		if(!empty($fromcategories)) {
			$select -> where("categoryid1 IN(".implode(",",$fromcategories).") OR categoryid2 IN(".implode(",",$fromcategories).")");
		}

			
		if($rows = $this->_db->fetchCol($select)) {
			
			$select = $this->_db->select()
					-> distinct()
					-> from(array("AA"=>"pw_categories"))
					-> join(array("BB"=>"pw_categories_acl"),"AA.categoryid=BB.categoryid",array())
					-> where("AA.isexist='yes'")
					-> where("AA.categoryid IN(".implode(",",$rows).")")
					-> order("AA.cleft");
					
			if($this->getModule()!="control") {
				$select -> where("AA.fullpath_visible='yes'")
						-> where("BB.acl_value!='deny'");
			}
			
			return $this->_db->fetchAll($select);
				
		}
			
		return false;
		
	}
	
	
	/**
	 * Возвращает братьев заданного раздела
	 *
	 * @param int $categoryid Id раздела
	 * @return array
	 */
	
	public function getBrothers($categoryid) {
		
		if($this->getModule()=='control') {
			$condition = array("and" => array("cparent=(SELECT cparent FROM pw_categories WHERE categoryid=".(int)$categoryid.")","isexist='yes'"));
		} else {
			$condition = array("and" => array("cparent=(SELECT cparent FROM pw_categories WHERE categoryid=".(int)$categoryid.")","fullpath_visible='yes'","isexist='yes'"));
		}
		
		return $this->_db->fetchAll($this->_dbtree->Full($this->_fields,$condition));
		
	}
	
	
	/**
	 * Возвращает id последнего брата заданного раздела
	 *
	 * @param int $categoryid Id раздела
	 * @return array
	 */
	
	public function getLastBrother($categoryid) {
		
		if($Brothers = $this->getBrothers($categoryid)) {
			return $Brothers[count($Brothers)-1]['categoryid'];			
		}
		
		return false;
		
	}
	
	
	/**
	 * Возвращает детей заданного раздела
	 *
	 * @param int $categoryid Id раздела
	 * @return array
	 */
	
	public function getChilds($categoryid) {
		
		if($this->getModule()=='control') {
			$condition =  array("and" => array("cparent=".(int)$categoryid,"isexist='yes'"));
		} else {
			$condition = array("and" => array("cparent=".(int)$categoryid,"fullpath_visible='yes'","isexist='yes'"));
		}
		
		return $this->_db->fetchAll($this->_dbtree->Full($this->_fields,$condition));
		
	}
	
	
	/**
	 * Возвращает путь до заданного раздела
	 *
	 * @param int $categoryid Id раздела
	 * @return array
	 */
	
	public function getBranches($categoryid) {
		
		if($this->getModule()=='control') {
			$condition =  array("and" => array("isexist='yes'"));
		} else {
			$condition = array("and" => array("fullpath_visible='yes'","isexist='yes'"));
		}
		
		return $this->_db->fetchAll($this->_dbtree->Branch((int)$categoryid,$this->_fields,$condition));
		
	}
	
	
	/**
	 * Возвращает путь до заданного раздела в виде одноуровнего массива Id разделов
	 *
	 * @param int $categoryid Id раздела
	 * @return array
	 */
	
	public function getBranchesPath($categoryid) {
		
		if($this->getModule()=='control') {
			$condition =  array("and" => array("isexist='yes'"));
		} else {
			$condition = array("and" => array("fullpath_visible='yes'","isexist='yes'"));
		}
		
		return $this->_db->fetchCol($this->_dbtree->Branch((int)$categoryid,array('categoryid'),$condition));
		
	}
	
	
	/**
	 * Возвращает информацию о разделе
	 * 
	 * @param int $categoryid Id раздела
	 * 
	 * @return array
	 */
		
	public function getCategoryInfo($categoryid) {
		
		$select = $this->_db->select()
				-> from(array('AA'=>'pw_categories'))
				-> joinLeft(array('BB'=>'pw_resources'),'AA.mainresourceid=BB.resourceid',array('mainresourcename'=>'resourcename'))
				-> joinLeft(array('CC'=>'pw_widgets_sets'),'AA.widgetssetid=CC.setid',array('widgetssetname'=>'setname'))
				-> joinLeft(array('DD'=>'pw_resources_modules'),'AA.moduleid=DD.moduleid',array('modulename','modulegenetivename'=>'genetivename','modulesinglename'=>'singlename'))
				-> where('AA.categoryid=?',$categoryid);
			
		if($row = $this->_db->fetchRow($select)) {
			
			/*	
			// Родители
			$Parents = $this->_dbtree->Parents($row['categoryid'],'all');
			$row['parents'] = $Parents->fetchAll();
				
			// Дети
			$row['childs'] = $this->getChilds($row['categoryid']);
				
			// Братья
			$row['brothers'] = $this->getBrothers($row['categoryid']);*/
			
			// Получаем дополнительное инфо по разделу
			return $this->_getAdditionalInfo($row);
				
		} else {
				
			return false;
				
		}
			
	}
	
	
	/**
	 * Возвращает объект меню разделов сайта
	 * 
	 * @param int $active Id активной категории
	 *
	 * @return Zend_Navigation
	 */
	
	public function getNavigationMenu($active = 0) {
		
		$List = $this->getRealList($active);
		
		$Filter = new Phorm_Filter_Tree();
		$Tree = $Filter->filter($List,'clevel','pages');
		
		$container = new Zend_Navigation($Tree);
		
		return $container;
		
	}
	
	
	/**
	 * Обновляет полный путь раздела, а также видимость всего пути
	 *
	 * @param int $categoryid
	 * @return boolean
	 * @todo Сделать апдейт всей нижележащей ветки, а не только текущего раздела
	 */
	
	private function updateFullPath($categoryid) {
		
		$select = $this->_db->select()
				-> from(array('AA'=>'pw_categories'), new Zend_Db_Expr("CONCAT(GROUP_CONCAT(AA.categorypath ORDER BY AA.cleft SEPARATOR '/'),'/') fullpath,
					IF(CONCAT(GROUP_CONCAT(IF(AA.visible='yes',AA.categorypath,'-') ORDER BY AA.cleft SEPARATOR '/'),'/') LIKE '%-%','no','yes') fullpath_visible,
					IF(CONCAT(GROUP_CONCAT(IF(AA.isexist='yes',AA.categorypath,'-') ORDER BY AA.cleft SEPARATOR '/'),'/') LIKE '%-%','no','yes') fullpath_exist"))
				-> joinInner(array('BB'=>'pw_categories'),'AA.cleft<=BB.cleft AND AA.cright>=BB.cright',array())
				-> where('BB.categoryid=?',$categoryid);
				
		if($row = $this->_db->fetchRow($select)) {
			
			return $this->_db->update('pw_categories',$row,'categoryid='.(int)$categoryid);
			
		}
		
		return false;
		
	}
	
	
	/**
	 * Возвращает Id предыдущего брата
	 * 
	 * @param array $CategoryInfo Массив данных о текущем разделе
	 * 
	 * @return int
	 */
		
	private function getPrevBrother($CategoryInfo) {
		
		$select = $this->_db->select()
				-> from('pw_categories',array('categoryid'))
				-> where('cparent=?',$CategoryInfo['cparent'])
				-> where('cleft<?',$CategoryInfo['cleft'])
				-> order('cleft DESC')
				-> limit(1);
			
		return $this->_db->fetchOne($select);
			
	}
	
	
	/**
	 * Возвращает Id следующего брата
	 * 
	 * @param array $CategoryInfo Массив данных о текущем разделе
	 * 
	 * @return int
	 */
		
	private function getNextBrother($CategoryInfo) {
		
		$select = $this->_db->select()
				-> from('pw_categories',array('categoryid'))
				-> where('cparent=?',$CategoryInfo['cparent'])
				-> where('cleft>?',$CategoryInfo['cleft'])
				-> order('cleft')
				-> limit(1);
			
		return $this->_db->fetchOne($select);
			
	}
	
	
	/**
	 * Меняет позицию раздела
	 * 
	 * @param array $CategoryInfo информация о текущем раздела
	 * @param string $direction Куда перемещать (up | down)
	 * 
	 * @return boolean
	 */
		
	public function changePosition($CategoryInfo,$direction) {
		
		$this->_db->beginTransaction();
		
		try {
		
			if($direction=='up' && $previd = $this->getPrevBrother($CategoryInfo)) {
				
				$this->_dbtree->ChangePositionAll($CategoryInfo['categoryid'],$previd,'before');
				
			} elseif($direction=='down' && $nextid = $this->getNextBrother($CategoryInfo)) {
				
				$this->_dbtree->ChangePositionAll($CategoryInfo['categoryid'],$nextid,'after');
				
			} else {
				
				$this->_db->rollBack();
				$this->_errors[] = $this->_translate->_('Некуда перемещать раздел');
				return false;
				
			}
			
			$this->updateFullPath($CategoryInfo['categoryid']);
			
			/**
			 * Логирование
			 */
			$this->setLog('shift',
				array('url' => '/control/categories/edit/categoryid/' . (int)$CategoryInfo['categoryid'] . '/',
					  'name' => $CategoryInfo['categoryname'],
					  'tablename' => 'pw_categories',
					  'identity' => (int)$CategoryInfo['categoryid'],
					  'fieldname' => 'categoryid'));
			
			/**
			 * Очистка кеша 
			 */
			$this->getCache('categories')->clean();
			
			$this->_db->commit();
			
			return true;
		
		} catch (Exception $e){
			
			$this->_db->rollBack();
			$this->_errors[] = $e->getMessage();
			
		}
			
		return false;
			
	}
	
	
	/**
	 * Возвращает форму создания/редактирования раздела
	 *
	 * @param int $categoryid Id текущего раздела 
	 * @param string $section Секция формы (add | edit)
	 * @return Phorm_Form
	 */
	
	public function getCategoryForm($categoryid, $section = 'add') {
		
		$form = new Phorm_Form('forms/control/categories.xml',$section);
		
		/**
		 * Добавляем справочники на элементы
		 */
		
		foreach ($form->getElements() as $element) {
			
			$elementname = $element->getName();
			
			switch ($elementname) {
					
				case 'moduleid':
					
					/**
					 * Список модулей
					 */
					
					// Используйте тип элемента Modules где список модулей накидывается внутри элемента
					
					break;
					
				case 'insertbefore':
					
					/**
					 * Список братьев или детей и позиция относительно них
					 */
					
					$Categories = $section=='add' ? $this->getChilds($categoryid) : $this->getBrothers($categoryid);
					
					foreach ($Categories as $key=>$category) {
						if($category['categoryid']==$categoryid) {
							if(key_exists($key+1,$Categories)) $element->setValue($Categories[$key+1]['categoryid']);
							continue;
						}
						$element->addMultiOption($category['categoryid'], $this->_translate->_('Перед') .' «' . $category['categoryname'] . '»');
					}
					
					$element->addMultiOption(0, $this->_translate->_('В конце'));
					
					if(!$element->getValue()) $element->setValue(0);
					
					break;
					
				case 'depends':
					
					/**
					 * Связанные разделы
					 */

					// Используйте тип элемента Categories где список разделов накидывается внутри элемента
					
					break;
					
				case 'realcategoryid':
					
					/**
					 * Раздел для замещения
					 */

					// Используйте тип элемента Categories где список разделов накидывается внутри элемента
					
					break;
					
				case 'categorypath':
					
					/**
					 * Путь раздела
					 */
					
					/*if($CategoryInfo = $this->getCategoryInfo($categoryid)) {
						$element->setLabel($element->getLabel() . ' (' . $CategoryInfo['fullpath'] . ')');
					}*/
					
					break;

			}
			
		}
		
		return $form;
		
	}
	
	
	/**
	 * Сохраняет данные формы создания/редактирования раздела
	 * 
	 * @param Phorm_Form $form Объект формы
	 * @param mixed $data Дополнительные данные формы. Если передана строка, то принимается как categoryid
	 * @param mixed $additionals Имя метода или массив имен для дополнительной обработки данных
	 * 							 Методы должны существовать в текущем классе (в т.ч. могут быть и в расширенном)
	 * 							 Используется для сохранения дополнительных данных разделов
	 * 							 Каждый из методов должен возвращать массив данных с модифицированными ключами 
	 * @param mixed $filters Имя метода или массив имен для предварительной фильтрации данных
	 * 						 Методы должны существовать в текущем классе (в т.ч. могут быть и в расширенном)
	 * 						 Каждый из методов должен возвращать массив данных с модифицированными ключами
	 * @see /application/configs/forms/control/categories.xml
	 * @return array | false
	 */
	
	public function saveCategoryForm(Phorm_Form $form, $data = null, $additionals = null, $filters = null) {
		
		/**
		 * Получаем значения формы и объединяем их с массивом дополнительных данных
		 */
		
		if(!is_null($data) && is_string($data)) $data = array('categoryid'=>$data);
		$data = array_merge($form->getValues(),(array)$data);
		
		/**
		 * Предварительный фильтр значений
		 */
		
		if(!$data = $this->_filterFormInfo($data,$filters)) return false;
		$maindata = $this->getFilteredValuesByTableCols('pw_categories',$data);
		
		/**
		 * Сохранение данных
		 */
		
		$this->_db->beginTransaction();
		
		try {
		
			if(isset($data['categoryid'])) {
				
				// Обновление существующей записи
				$this->_db->update('pw_categories',$maindata,'categoryid='.(int)$data['categoryid']);
				$action = 'update';
				
			} elseif(isset($data['parentid'])) {
				
				// Создание нового раздела
				$data['categoryid'] = $this->_dbtree->Insert($data['parentid'],null,$maindata);
				$action = 'insert';
				
			}
			
			/**
			 * Дополнительные данные раздела
			 */
			
			if(!$data = $this->_saveAdditionalInfo($data,$additionals)) {
				$this->_db->rollBack();
				return false;
			}
			
			/**
			 * Поисковый индекс
			 */
			
			$this->buildSearchIndex(array('indextitle' => $data['categoryname'],
										  'indextxt'   => $data['categorytxt'],
										  'categoryid' => $data['categoryid']));
										  
			/**
			 * Логирование
			 */
			$this->setLog($action,
				array('url' => '/control/categories/edit/categoryid/' . (int)$data['categoryid'] . '/',
					  'name' => $data['categoryname'],
					  'tablename' => 'pw_categories',
					  'identity' => (int)$data['categoryid'],
					  'fieldname' => 'categoryid'));
			
			/**
			 * Очистка кеша 
			 */
			$this->getCache('categories')->clean();
			
			$this->_db->commit();
			
			return $data;
		
		} catch (Exception $e) {
			
			$this->_db->rollBack();
			$this->_errors[] = $e->getMessage();
			
		}
		
		return false;
		
	}
	
	
	/**
	 * Удаляет раздел
	 *
	 * @param array $CategoryInfo Массив данных о разделе
	 * @return boolean
	 * @todo Сделать удаление всей цепочки разделов
	 */
	
	public function deleteCategory($CategoryInfo) {
		
		$this->_db->beginTransaction();
		
		try {
		
			$this->_db->update('pw_categories',array('isexist'=>'no'),'categoryid='.$CategoryInfo['categoryid']);
			$this->updateFullPath($CategoryInfo['categoryid']);
			
			/**
			 * Логирование
			 */
			$this->setLog('delete',
				array('url' => '/control/categories/edit/categoryid/' . (int)$CategoryInfo['categoryid'] . '/',
					  'name' => $CategoryInfo['categoryname'],
					  'tablename' => 'pw_categories',
					  'identity' => (int)$CategoryInfo['categoryid'],
					  'fieldname' => 'categoryid'));
			
			/**
			 * Очистка кеша 
			 */
			$this->getCache('categories')->clean();
			
			$this->_db->commit();
			
			return true;
		
		} catch (Exception $e) {
			
			$this->_db->rollBack();
			$this->_errors[] = $e->getMessage();
			
		}
		
		return false;
		
	}
	
	
	/**
	 * Проверяет, может ли пользователь выполнить определенное действие над разделом
	 * 
	 * @param string $action Наименование действия ("view","add","edit","delete","move","shift")
	 * @param int $categoryid Id раздела
	 *  
	 * @return boolean
	 * @todo Сейчас вы возвращает true при уровне доступа больше 900, мб сделать разделение на подуровни @see /application/templates/control/categories.tpl
	 */

	public function isAllowedAction($action, $categoryid) {
		
		$return = false;
				
			switch ($action) {
						
			case "view":
					
				/*if(isset($this->_usersession->UserData["acl"]["categories"][$categoryid]) 
					  && $this->_usersession->UserData["acl"]["categories"][$categoryid]>900) {
					return true;
				}*/
				
				return true;
								
				break;
							
			case "add":
							
				if(isset($this->_usersession->UserData["acl"]["categories"][$categoryid]) 
					  && $this->_usersession->UserData["acl"]["categories"][$categoryid]>900) {
					return true;
				}
								
				break;

								
			case "edit":
								
				if(isset($this->_usersession->UserData["acl"]["categories"][$categoryid]) 
					  && $this->_usersession->UserData["acl"]["categories"][$categoryid]>900) {
					return true;
				}
								
				break;
								
			case "delete":
								
				if(isset($this->_usersession->UserData["acl"]["categories"][$categoryid]) 
					  && $this->_usersession->UserData["acl"]["categories"][$categoryid]>900) {
					return true;
				}
								
				break;
				
			case "move":
								
				if(isset($this->_usersession->UserData["acl"]["categories"][$categoryid]) 
					  && $this->_usersession->UserData["acl"]["categories"][$categoryid]>900) {
					return true;
				}
								
				break;
				
			case "shift":
								
				if(isset($this->_usersession->UserData["acl"]["categories"][$categoryid]) 
					  && $this->_usersession->UserData["acl"]["categories"][$categoryid]>900) {
					return true;
				}
								
				break;
								
		}
			
		return $return;
			
	}
	
	
	/**
	 * МЕТОДЫ, ВОЗВРАЩАЮЩИЕ ДОПОЛНИТЕЛЬНУЮ ИНФОРМАЦИЮ О РАЗДЕЛЕ
	 * МЕТОДЫ ОПРЕДЕЛЕНЫ КАК protected, ПОСКОЛЬКУ ВОЗВРАЩАЮТ ЧИСТЫЕ ЗНАЧЕНИЯ ДЛЯ ФОРМ
	 */
	
	/**
	 * Возвращает модифицированный массив информации о разделе с дополнительной информацией
	 *
	 * @param array $categoryinfo Информация о разделе @see self::getCategoryInfo
	 * @return array
	 */
	
	protected function _getAdditionalInfo($categoryinfo) {
		
		/**
		 * Десериализация extradata
		 */
		
		if(!empty($categoryinfo['extradata'])) {
			$categoryinfo['extradata'] = Zend_Json::decode($categoryinfo['extradata']);
		}
		
		/**
		 * Десериализация metadata
		 */
		
		if(!empty($categoryinfo['metadata'])) {
			$categoryinfo['metadata'] = Zend_Json::decode($categoryinfo['metadata']);
		}
		
		/**
		 * Получение дополнительной информации в зависимости от значений в metadata
		 * @todo Не выбирать данные если о них нет информации в metadata поскольку это ведет к постоянной выборке images и files
		 */
		
		$methods = array(
			'images' => '_getImages',
			'files' => '_getFiles',
			'depends' => '_getDepends',
			'acl' => '_getAcl',
		);
		
		foreach ($methods as $field=>$methodname) {
			if((isset($categoryinfo['metadata'][$field]) && $categoryinfo['metadata'][$field]>0) 
					|| !isset($categoryinfo['metadata'][$field])) {
				if($info = $this->$methodname($categoryinfo['categoryid'])) $categoryinfo[$field] = $info;
			}
		}
		
		/**
		 * Корректировка images to image
		 */
		
		if(isset($categoryinfo['images']) && count($categoryinfo['images'])===1) {
			$categoryinfo['image'] = $categoryinfo['images'][0]['mediaid'];
		}
		
		/**
		 * Корректировка files to file
		 */
		
		if(isset($categoryinfo['files']) && count($categoryinfo['files'])===1) {
			$categoryinfo['file'] = $categoryinfo['files'][0]['mediaid'];
		}
			
		return $categoryinfo;
		
	}
	
	/**
	 * Возвращает дополнительную информацию об изображениях в разделе
	 *
	 * @param int $categoryid
	 * @return array
	 */
	
	protected function _getImages($categoryid) {
		
		$select = $this->_db->select()
				-> from('pw_categories_media')
				-> where('categoryid=?',$categoryid)
				-> where('mediatype=?','image')
				-> order('sortorder');
					
		return $this->_db->fetchAll($select);
		
	}
	
	/**
	 * Возвращает дополнительную информацию о файлах в разделе
	 *
	 * @param int $categoryid
	 * @return array
	 * @todo Реализовать
	 */
	
	protected function _getFiles($categoryid) {
		
		$select = $this->_db->select()
				-> from('pw_categories_media')
				-> where('categoryid=?',$categoryid)
				-> where('mediatype!=?','image')
				-> order('sortorder');
					
		return $this->_db->fetchAll($select);
		
	}
	
	/**
	 * Возвращает список связанных разделов
	 *
	 * @param int $categoryid
	 * @return array
	 */
	
	protected function _getDepends($categoryid) {
		
		$select = $this->_db->select()
				-> from('pw_categories_depends', new Zend_Db_Expr('IF(categoryid1='.(int)$categoryid.',categoryid2,categoryid1)'))
				-> where('categoryid1=?',$categoryid)
				-> OrWhere('categoryid2=?',$categoryid);
		
		return $this->_db->fetchCol($select);
		
	}
	
	/**
	 * Возвращает ACL-таблицу раздела
	 *
	 * @param int $categoryid
	 * @return array
	 */
	
	protected function _getAcl($categoryid) {
		
		$select = $this->_db->select()
				-> from(array('AA'=>'pw_roles'),array('roleid'))
				-> joinLeft(array('BB'=>'pw_categories_acl'),'AA.roleid=BB.roleid',array('accesslevel'))
				-> where('BB.categoryid=?',$categoryid);
		
		return $this->_db->fetchPairs($select);
		
	}
	
	
	/**
	 * МЕТОДЫ, ФИЛЬТРУЮЩИЕ ДАННЫЕ РАЗДЕЛА ПЕРЕД СОХРАНЕНИЕМ
	 * МЕТОДЫ ОПРЕДЕЛЕНЫ КАК protected, ПОСКОЛЬКУ ИСПОЛЬЗУЮТСЯ ПРИ СОХРАНЕНИИ РАЗДЕЛА
	 * @todo Рассмотреть необходимость изменения видимости методов на public
	 */
	
	/**
	 * Предварительный фильтр данных раздела
	 *
	 * @param array $categoryinfo Массив фильтруемых данных
	 * @param mixed $filters Массив или строка пользовательских методов
	 * @return array
	 */
	
	protected function _filterFormInfo($categoryinfo, $filters = null) {
		
		/**
		 * Системные фильтры
		 */
		
		/**
		 * Одно изображение
		 */
		
		if(isset($categoryinfo['image'])) {
			if(is_array($categoryinfo['image'])) {
				$categoryinfo['images'] = $categoryinfo['image'];
			} else {
				$categoryinfo['images'] = array(array('mediaid'=>$categoryinfo['image']));
			}
		}
		
		/**
		 * Один файл
		 */
		
		if(isset($categoryinfo['file'])) {
			if(is_array($categoryinfo['file'])) {
				$categoryinfo['files'] = $categoryinfo['file'];
			} else {
				$categoryinfo['files'] = array(array('mediaid'=>$categoryinfo['file']));
			}
		}
		
		/**
		 * Extradata
		 */
		
		if(isset($categoryinfo['extradata']) && is_array($categoryinfo['extradata'])) {
			$joined = join('',$categoryinfo['extradata']);
			if(empty($joined)) {
				$categoryinfo['extradata'] = null;
			} else {
				$categoryinfo['extradata'] = Zend_Json::encode($categoryinfo['extradata']);
			}
		}
		
		/**
		 * Пользовательские фильтры
		 */
		
		if(!is_null($filters)) {
			if(is_string($filters)) $filters = array($filters);
			if(is_array($filters)) {
				foreach ($filters as $methodname) {
					if(method_exists($this,$methodname)) {
						if(!$categoryinfo = $this->$methodname($categoryinfo)) {
							$this->_errors[] = 'Ошибка фильтрации данных в методе ' . $methodname;
							return false;
						}
					} else {
						$this->_errors[] = 'Не найден метод ' . $methodname;
						return false;
					}
				}
			} else {
				$this->_errors[] = 'Невозможно определить методы для фильтрации данных';
				return false;
			}
		}
		
		/**
		 * Metadata - просто считаем кол-во элементов в полях, где значения являются массивами
		 */
		
		$categoryinfo['metadata'] = array();
		foreach ($categoryinfo as $field=>$value) {
			if(is_array($value)) $categoryinfo['metadata'][$field] = count($value);
		}
		if(empty($categoryinfo['metadata'])) {
			$categoryinfo['metadata'] = null;
		} else {
			$categoryinfo['metadata'] = Zend_Json::encode($categoryinfo['metadata']);
		}
		
		return $categoryinfo;
		
	}
	
	
	/**
	 * МЕТОДЫ, СОХРАНЯЮЩИЕ ДОПОЛНИТЕЛЬНУЮ ИНФОРМАЦИЮ О РАЗДЕЛЕ
	 * МЕТОДЫ ОПРЕДЕЛЕНЫ КАК protected, ПОСКОЛЬКУ ИСПОЛЬЗУЮТСЯ ПРИ СОХРАНЕНИИ РАЗДЕЛА
	 * МЕТОДЫ ДОЛЖНЫ ВЫЗЫВАТЬСЯ В РАМКАХ SQL-ТРАНСАКЦИИ
	 * @todo Рассмотреть необходимость изменения видимости методов на public
	 */
	
	
	/**
	 * Сохраняет дополнительную информацию о разделе
	 *
	 * @param array $categoryinfo Информация о разделе @see self::saveCategoryForm
	 * @param mixed $additionals Массив пользовательских методов
	 * @return array
	 * @todo Подумать над добавлением пользовательских методов по аналогии с материалами
	 */
	
	protected function _saveAdditionalInfo($categoryinfo, $additionals = null) {
		
		/**
		 * Системные методы
		 */
		
		$categoryinfo = $this->_saveImages($categoryinfo);
		$categoryinfo = $this->_saveFiles($categoryinfo);
		$categoryinfo = $this->_saveAcl($categoryinfo);
		$categoryinfo = $this->_saveDepends($categoryinfo);
		$categoryinfo = $this->_savePosition($categoryinfo);
		
		$this->updateFullPath($categoryinfo['categoryid']);
		
		/**
		 * Пользовательские методы
		 */
		
		if(!is_null($additionals)) {
			if(is_string($additionals)) $additionals = array($additionals);
			if(is_array($additionals)) {
				foreach ($additionals as $methodname) {
					if(method_exists($this,$methodname)) {
						if(!$categoryinfo = $this->$methodname($categoryinfo)) {
							$this->_errors[] = 'Ошибка сохранения в методе ' . $methodname;
							return false;
						}
					} else {
						$this->_errors[] = 'Не найден метод ' . $methodname;
						return false;
					}
				}
			} else {
				$this->_errors[] = 'Невозможно определить методы для обработки дополнительных данных';
				return false;
			}
		}
			
		return $categoryinfo;
		
	}
	
	
	/**
	 * Сохраняет связку раздела с изображениями
	 *
	 * @param array $data Массив данных о разделе
	 * @return array
	 */
	
	protected function _saveImages($data) {
		
		/**
		 * Отвязываем от раздела все изображения
		 */
		
		$this->_db->delete('pw_categories_media','categoryid=' . (int)$data['categoryid'] . " AND mediatype='image'");
		
		/**
		 * Массив изображений
		 */
		
		if(isset($data['images'])) {
			
			/**
			 * Вставляем массив картинок
			 */
			
			if(is_array($data['images'])) {
				
				require_once 'Phorm/Media.php';
				$Media = new Phorm_Media();
				
				$images = $this->getFilteredValuesByTableCols('pw_categories_media',$data['images']);
					
				$itr = 0;
				foreach ($images as $image) {
					if($MediaInfo = $Media->getMediaInfo($image['mediaid'])) {
						$image['sortorder'] = $itr++;
						$image['mediatype'] = 'image';
						$image['categoryid'] = $data['categoryid'];
						$image['filepath'] = $MediaInfo['filepath'];
						$image['fileext'] = $MediaInfo['fileext'];
						$this->_db->insert('pw_categories_media',$image);
					}
				}
			
			}
					
		}
		
		return $data;
		
	}
	
	/**
	 * Сохраняет связку раздела с файлами
	 *
	 * @param array $data Массив данных о разделе
	 * @return array
	 */
	
	protected function _saveFiles($data) {
		
		/**
		 * Отвязываем от раздела все файлы
		 */
		
		$this->_db->delete('pw_categories_media','categoryid=' . (int)$data['categoryid'] . " AND mediatype!='image'");
		
		/**
		 * Массив файлов
		 */
		
		if(isset($data['files'])) {
			
			/**
			 * Вставляем массив файлов
			 */
			
			if(is_array($data['files'])) {
				
				require_once 'Phorm/Media.php';
				$Media = new Phorm_Media();
				
				$files = $this->getFilteredValuesByTableCols('pw_categories_media',$data['files']);
					
				$itr = 0;
				foreach ($files as $file) {
					if($MediaInfo = $Media->getMediaInfo($file['mediaid'])) {
						$file['sortorder'] = $itr++;
						$file['mediatype'] = $MediaInfo['mediatype'];
						$file['categoryid'] = $data['categoryid'];
						$file['filepath'] = $MediaInfo['filepath'];
						$file['fileext'] = $MediaInfo['fileext'];
						$this->_db->insert('pw_categories_media',$file);
					}
				}
			
			}
					
		}
		
		return $data;
		
	}
	
	/**
	 * Сохраняет ACL-таблицу раздела
	 *
	 * @param array $data Массив данных о разделе
	 * @return array
	 */
	
	protected function _saveAcl($data) {
			
		$this->_db->delete('pw_categories_acl','categoryid='.(int)$data['categoryid']);
			
		if(isset($data['acl']) && is_array($data['acl'])) {
				
			foreach ($data['acl'] as $roleid=>$accesslevel) {
				$this->_db->insert('pw_categories_acl',array(
										'categoryid' => $data['categoryid'],
										'roleid' => $roleid,
										'accesslevel' => $accesslevel));
			}
				
		}
		
		return $data;
		
	}
	
	/**
	 * Сохраняет связку раздела с другими разделами
	 *
	 * @param array $data Массив данных о разделе
	 * @return array
	 */
	
	protected function _saveDepends($data) {
			
		$this->_db->delete('pw_categories_depends',(int)$data['categoryid'].' IN(categoryid1,categoryid2)');
			
		if(isset($data['depends'])) {
			foreach ($data['depends'] as $dependid) {
				if($data['categoryid']===$dependid) continue;
				$this->_db->insert('pw_categories_depends',array('categoryid1'=>$data['categoryid'],'categoryid2'=>$dependid));
			}
		}
		
		return $data;
		
	}
	
	/**
	 * Сохраняет позицию раздела относительно своих братьев
	 *
	 * @param array $data Массив данных о разделе
	 * @return array
	 */
	
	protected function _savePosition($data) {
			
		if(isset($data['insertbefore']) && $data['insertbefore']>0) {
			$this->_dbtree->ChangePositionAll($data['categoryid'],$data['insertbefore'],'before');
		} elseif ($data['insertbefore']==0 && $LastBrother = $this->getLastBrother($data['categoryid'])) {
			$this->_dbtree->ChangePositionAll($data['categoryid'],$LastBrother,'after');
		}
		
		return $data;
		
	}
	
}